# -*- coding: utf-8 -*-

#
# fusion.http.FusionApp
#
# Copyright 2008 Helsinki Institute for Information Technology
# and the authors.
#
# Authors: Harri Hämäläinen <hhamalai@iki.fi>
#

# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

from fusion.db.exceptions import Unauthorized
from fusion.db.path import decode_text
from fusion.db.profile import Profile
from fusion.http.util import get_fields
from string import Template
from types import StringType
import cgi, hashlib, logging, simplejson


invitations = {}

class Response(object):
    statuscodes = {
        200: 'OK',
        201: 'Created',
        202: 'Accepted',
        203: 'Non-Authoritative Information',
        204: 'No Content',
        205: 'Reset Content',
        206: 'Partial Content',

        300: 'Multiple Choices',
        301: 'Moved Permanently',
        302: 'Found',
        303: 'See Other',
        304: 'Not Modified',

        400: 'Bad Request',
        401: 'Unauthorized',
        402: 'Payment Required',
        403: 'Forbidden',
        404: 'Not Found',
        405: 'Method Not Allowed',
        406: 'Not Acceptable',
        407: 'Proxy Authentication Required',
        408: 'Request Timeout',
        409: 'Conflict',
        410: 'Gone',
        411: 'Length Required',
        412: 'Precondition Failed',
        413: 'Request Entity Too Large',
        414: 'Request-URI Too Long',
        415: 'Unsupported Media Type',
        416: 'Requested Range Not Satisfiable',

        500: 'Server Error',
        501: 'Not Implemented',
        502: 'Bad Gateway',
        503: 'Service Unavailable',
        504: 'Gateway Timeout',
        505: 'HTTP Version Not Supported',
        506: 'Variant Also Negotiates',
        507: 'Insufficient Storage'
    }

    def __init__(self, status=200, type=''):
        self.headers = {}
        self.output = []
        self.type = type
        self.status = '%s %s' % (status, self.statuscodes[status])
        self.headers['Cache-Control'] = 'no-cache'

    def add_header(self, key, value):
        self._remove_key(key)
        self.headers[key] = value

    def append_output(self, value):
        self.output.append(value)

    def make_response(self, start_response):
        if not self.type:
            pass

        elif self.type =='json':
            self.output = [simplejson.dumps(value) for value
                           in self.output]
            self.add_header('Content-Type', 'text/x-json')

        elif self.type == 'text':
            self.add_header('Content-Type', 'text/plain; charset=UTF-8')
            self.output = [v if decode_text(v) else '' for v
                           in self.output]

        elif self.type == 'sha1-hex':
            self.output = [hashlib.sha1(value).hexdigest() for value
                           in self.output]
            self.add_header('Content-Type', 'text/plain')

        elif self.type == 'sha1':
            self.output = [hashlib.sha1(value).digest() for value
                           in self.output]
            self.add_header('Content-Type', 'application/octet-stream')

        else:
            logging.warn("Type is unknown: %s" % self.type)
            self.status = '%s %s' % (500, self.statuscodes[500])

        start_response(self.status, [(k, v) for k, v in self.headers.items()])

        return self.output

    def _remove_key(self, key):
        if key.lower in self.headers.keys():
            self.headers.pop(key)

    def _RESPONSE_(self):
        pass


def stylesheet_tag(style):
    return '''
    <link type="text/css" href="/static/css/%s.css" rel="stylesheet" title="Fusion %s" />
    ''' % (style, style.capitalize())


class FusionApp(object):
    def __init__(self, environ, start_response):
        self.environ = environ
        self.start = start_response
        self._headers = {}
        self._output = []
        self._notifications = {'default': []}
        self.db = environ['fusion.db']
        self.querydict = cgi.parse_qs(self.environ.get('QUERY_STRING'))
        self.method = self.environ.get('REQUEST_METHOD')
        self.iid = self.environ.get('fusion.iid')
        self.path = self.environ.get('PATH_INFO', '')
        self.scriptname = self.environ.get('SCRIPT_NAME', '')


    def __iter__(self):
        '''
        Overriding __iter__ in subclasses might not be a good idea
        '''
        if hasattr(self, 'do_'+self.environ.get('REQUEST_METHOD')):
            response = getattr(self, 'do_'+self.environ.get('REQUEST_METHOD'))()
            if hasattr(response, '_RESPONSE_'):
                for result in response.make_response(self.start):
                    if type(result) is StringType:
                        yield result
                    else:
                        logging.warn("Ignoring nonstring output (forgot to encode?)")
            else:
                for result in response:
                    if type(result) is StringType:
                        yield result
                    else:
                        logging.warn("Ignoring nonstring output (forgot to encode?)")
        else:
            yield ''.join(Response(501).make_response())


    def get(self, iid=None):
        '''
        This can be called if inherited application needs basic get 
        method functionality
        
        Helper method for pure data requests. Output parameter specifies
        desired output type: ascii (default), json, ?

        Returns ready Response object
        '''
        
        def lookup(key):
            return self.db.get_text(self.iid, '%s%s' % (self.path, key))
        
        if not iid:
            iid = self.iid
        v = None
        response = Response()
        
        dom_cipher = None
        if self.querydict.get('auto_crypt'):
            try:
                dom_cipher = self.db.get_domain_cipher(iid)
            except LookupError:
                assert dom_cipher == None
                pass
            
        output_type = self.querydict.get('output') or []
        if len(output_type) > 0:
            output_type = output_type[0]
            
        if output_type == 'json':
            if self.path.endswith('/'):
                
                keys = self.db.keys(iid, self.path)
                dataset_info = self.db.get_json(iid, '')
                result = {'mark': self.db.get_last_mark(),
                          'parent': dataset_info.get('parent')}
                
                if self.querydict.get('parent'):
                    pass
                elif self.querydict.get('keys-only'):
                    result['keys'] = keys
                elif self.querydict.get('values-only'):
                    result['values'] = [lookup(k) for k in keys]
                else:
                    result['data'] = [(k, lookup(k)) for k in keys]
 
                v = simplejson.dumps(result, indent=2, sort_keys=True)

            else:
                v = self.db.get_json(iid, self.path, cipher = dom_cipher)
                if not v:
                    v = 'null'
                
        elif output_type == 'text':
            v = self.db.get_text(iid, self.path, cipher = dom_cipher)

        else:
            v = self.db.get(iid, self.path, cipher = dom_cipher)

        if v is not None:
            response.append_output(v)
        else:
            response = Response(404)

        return response


    def put(self):
        '''
        This can be called if inherited application needs basic put 
        method functionality
        
        First part of the real request path belongs to SCRIPT_NAME, and should
        be /content/ for each put/post/delete operation. In other words, in
        order to put key into dataset with path /foo/bar, you make request to
        path /content/foo/bar
        This hopefully will reduce the chance to mess up with SCRIPT_NAMEs
        reserved for server handlers and other applications.
        '''
        return self.post_or_put()


    def post(self):
        '''
        This can be called if inherited application needs basic post 
        method functionality
        
        To post form content into Fusion DB, specific prefix naming should be
        used with form field names to tell the desired operation. Prefix is
        separated from rest of the name with colon. String part after colon
        can be used as key.

        Available prefixes are:
        put: use the string part after colon in name attribute as key and
        anything in that field as value for that key

        put_filename: use the string part after colon in name attribute as key
        and the filename as value for that key

        put_file_under_name: use filename from value field as key and the file
        content as value
        '''
        return self.post_or_put()


    def delete(self):
        '''
        This can be called if inherited application needs basic delete 
        method functionality
        '''
        try:
            self.db.delete(self.iid, self.path)
            return Response()
        except Unauthorized:
            return Response(401)


    def read_request_form(self):
        try:
            return cgi.FieldStorage(fp=self.environ['wsgi.input'],
                                    environ=self.environ.copy(),
                                    keep_blank_values=True)
        except:
            raise ValueError


    def read_request_data(self):
        length = 0
        try:
            length = int(self.environ.get('CONTENT_LENGTH'))
        except (TypeError, ValueError):
            logging.warning('''request_data: invalid content length,
                            trunkating to zero''')
            length = 0
        return self.environ['wsgi.input'].read(length)


    def post_or_put(self):
        content_type = self.environ.get('CONTENT_TYPE', '').split(';')[0]
        if not content_type:
            logging.warn('Content type not specified')
            return Response(500)
        elif content_type == 'multipart/form-data':
            form = self.read_request_form()
        else:
            form = None

        try:
            if self.method == 'PUT' and form is None:
                self.process_data('PUT', self.read_request_data())
            elif self.method == 'POST' and form is None:
                self.process_data('POST', self.read_request_data())
            elif self.method == 'POST' and form is not None:
                self.process_form(form)
            else:
                raise ValueError
        except Unauthorized:
            response = Response(403)
            response.append_output('Unauthorized')
            return response

        response = Response()

        next_location = self.querydict.get('continue', [])
        if len(next_location) > 0:
            response = Response(303)
            response.add_header('Location', next_location[0])

        return response


    def process_data(self, method, data):
        if not self.path:
            logging.warn('No path specified for data, unable to process')
            return Response()
 
        if method == 'DELETE' or \
                self.environ.get('HTTP_X_HTTP_METHOD_OVERRIDE') == 'DELETE' or \
                data == '_method=delete':
            return self.delete()

        inputtype = ''
        if self.querydict.get('input'):
            inputtype = self.querydict.get('input')[0]

        dom_cipher = None
        if self.querydict.get('auto_crypt'):
            try:
                dom_cipher = self.db.get_domain_cipher(self.iid)
            except LookupError:
                assert dom_cipher == None
                pass

        if method == 'PUT' or \
                self.environ.get('HTTP_X_HTTP_METHOD_OVERRIDE') == 'PUT':
            if inputtype == 'json':
                self.db.put_json(self.iid, self.path, simplejson.loads(data),
                                 cipher = dom_cipher)
            else:
                self.db.put(self.iid, self.path, data, cipher = dom_cipher)
        else:
            if inputtype == 'json':
                self.db.post_json(self.iid, self.path, simplejson.loads(data),
                                  cipher = dom_cipher)
            else:
                self.db.post(self.iid, self.path, data, cipher = dom_cipher)


    def process_form(self, form, path='', iid=None):
        if iid == None:
            iid = self.iid

        dom_cipher = None
        if self.querydict.get('auto_crypt'):
            try:
                dom_cipher = self.db.get_domain_cipher(self.iid)
            except LookupError:
                assert dom_cipher == None
                pass
        
        view = self.db.get_view(iid, cipher = dom_cipher)
        json_data = {}
        for field_name in form:
            if ':' in field_name:
                op, path = field_name.split(':', 1)
                for field in get_fields(form, field_name):
                    if op == 'put':
                        v = field.value
                    elif op == 'put_filename':
                        if not field.filename:
                            continue
                        v = field.filename
                    elif op == 'put_file_under_name':
                        if not field.filename:
                            continue
                        v = field.value
                        path = '%s/%s' % (path, field.filename)
                    else:
                        logging.warn('Unknown oper %s' % op)
                        continue

                    if '/-/' not in path:
                        view[path] = v
                    else:

                        path, rest = path.split('/-/', 1)
                        d = json_data.setdefault(path, {})
                        if v:
                            self._insert_in_hierarchy(d, rest, v)

        for path in json_data:
            d = json_data[path]
            v = simplejson.dumps(d, sort_keys=True, indent=2)
            view[path] = v


    

    def request_url(self, host=True, path=True):
        from urllib import quote
        url = ''
        if host:
            url = self.environ['wsgi.url_scheme']+'://'
            url += self.environ['HTTP_HOST']
        if path:
            url += quote(self.environ.get('SCRIPT_NAME', ''))
            url += quote(self.environ.get('PATH_INFO', ''))
            if self.environ.get('QUERY_STRING'):
                url += '?' + self.environ['QUERY_STRING']
        return url

    def match_response(self, patterns):
        script = self.environ.get('SCRIPT_NAME')
        if script in patterns:
            return apply(patterns[script])
        else:
            return Response(500)
    
    def get_encrypted_data(self, path, dom_cipher):
        try:
            return self.db.get(self.iid, path, cipher=dom_cipher)
        except ValueError:
            return {}

    
    def add_notification(self, msg, target='default'):
        if self._notifications.has_key(target):
            self._notifications[target].append(msg)
        else:
            self._notifications[target] = [msg]

            
    def get_notifications(self, target=None):
        if target:
            return self._notifications.get(target, [])
        return []
    
    
    def invitation(self, form):
        global invitations
        if len(invitations) > 0:    
            response = Response(301)
            response.add_header('Location', 'http://admin.hexlabel.net/pending_invitation?show_delete=T')
            return response
        elif form.getvalue('invitation_code'):
            invitation_code = form.getvalue('invitation_code').strip()
            invitation = None
            
            try:
                invitation = self.db.fetch_invitation(invitation_code)
            except LookupError:
                self.add_notification('Connection error, try again',
                                      target='invitation_code')
                return
            except:
                self.add_notification('Protocol error, try again', target='invitation_code')
            
            if not invitation:
                self.add_notification('No such invitation code',
                                      target='invitation_code')
            
            elif invitation.iid == self.db.get_my_iid():
                self.add_notification('''Invitation code was created by 
                                      you and is now removed''',
                                      target='invitation_code')
            
            else:
                invitations[invitation.iid] = invitation
                self.db.subscribe(invitation.iid)
                response = Response(301)
                response.add_header('Location',
                            'http://admin.hexlabel.net/pending_invitation')
                return response
    
    
    
    def create_page(self, title, content, style=''):
        global invitations
        T_page = Template('''
        <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
            "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
        <html xmlns="http://www.w3.org/1999/xhtml">
            <head>
            <title>${title}</title>
            ${css}
            <style>
.safetycolor
{
	border-color: ${safetycolor};
}
        
            </style>
            </head>
            <body>
            <div class="topbar inputframe safetycolor">
                <div class="friendpanel">
                    <form method="POST" action="">
                    Invitation code:
                        <input type="text" name="invitation_code" />
                        <input type="submit" name="get_invitation" value="Go" />
                    </form>
                    <a href="http://admin.hexlabel.net/create_invitation">Invite friends</a>
                </div>
                <h1><img src="/static/fusion.png" alt="P2P-Fusion"</h1>
                <a href="http://my.hexlabel.net/?">My profile</a>
                (<a href="http://my.hexlabel.net/?edit=T">edit</a>)
                ${inv_code_notification}
            </div>
            <div id="notifications">
            ${notifications}
            </div>
            <div class="body">
            ${content}
            </div>
            <div class="footer">
            <hr />
            <a href="http://p2p-fusion.org/">About P2P-Fusion</a>
            <a href="http://admin.hexlabel.net/create_personal_code">Create login</a>
            </div>
            </body>
            </html>''')

        styles = []

        if style:
            styles.append(style)

        styles.append('default')
        
        stylesheets = ''.join([stylesheet_tag(style) 
                       for style
                       in styles])
        return T_page.substitute(title = title,
                safetycolor = self.db.get_safety_color(),
                notifications = self._get_notifications(),
                inv_code_notification = self._get_notifications(\
                                                    target='invitation_code'),
                content = content,
                css = stylesheets)


    def _get_notifications(self, target='default'):
        notifications = list(self._notifications.get(target, ''))
        if target == 'default':
            if len(invitations) > 0:
                iid = ''
                for k in invitations.keys():
                    iid = k
                if self.db.get_info(iid).get('type') != None:
                    notifications.append('''You have a <a 
                        href="http://%s.hexlabel.net/">pending invitation</a>
                        ''' % iid)
                else:
                    notifications.append('''
                    You have a 
                    <a href="http://admin.hexlabel.net/pending_invitation?show_options=T">
                    pending invitation</a>''')
            
            profile = Profile()
            profile.read(self.db, self.db.get_my_iid())
            
            if not (profile.list_section_names() or profile.pic or profile.about):
                notifications.append('''
                <a href="http://my.hexlabel.net/?edit=T">
                Your profile</a> is empty!''')
                
            return '%s' % '<br />'.join(notifications)
        else:
            return '<br />'.join(notifications)
            
    
    def _insert_in_hierarchy(self, parent, k, v):
        if isinstance(parent, dict):
            if not '/' in k:
                parent[k] = v
            else:
                k, rest = k.split('/', 1)
                child = parent.setdefault(k, {})
                self._insert_in_hierarchy(child, rest, v)

